package com.shujia.core

import org.apache.flink.api.common.eventtime.WatermarkStrategy
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.connector.kafka.source.KafkaSource
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer
import org.apache.flink.contrib.streaming.state.EmbeddedRocksDBStateBackend
import org.apache.flink.streaming.api.CheckpointingMode
import org.apache.flink.streaming.api.environment.CheckpointConfig.ExternalizedCheckpointCleanup
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector

object Demo12ValueState {
  def main(args: Array[String]): Unit = {

    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment



    // 每 1000ms 开始一次 checkpoint
    env.enableCheckpointing(5000)

    // 高级选项：

    // 设置模式为精确一次 (这是默认值)
    env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)

    // 确认 checkpoints 之间的时间会进行 500 ms
    env.getCheckpointConfig.setMinPauseBetweenCheckpoints(500)

    // Checkpoint 必须在一分钟内完成，否则就会被抛弃
    env.getCheckpointConfig.setCheckpointTimeout(60000)

    // 允许两个连续的 checkpoint 错误
    env.getCheckpointConfig.setTolerableCheckpointFailureNumber(2)

    // 同一时间只允许一个 checkpoint 进行
    env.getCheckpointConfig.setMaxConcurrentCheckpoints(1)

    // 使用 externalized checkpoints，这样 checkpoint 在作业取消后仍就会被保留
    env.getCheckpointConfig.setExternalizedCheckpointCleanup(
      ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION)


    /**
      * 设置checkpoint保存数据方式和位置
      *
      */
    //老版本
    //env.setStateBackend(new RocksDBStateBackend("hdfs://master:9000/flink/checkpoint/"))

    //新版本
    //rocksDB状态后端
    env.setStateBackend(new EmbeddedRocksDBStateBackend(true))
    //env.setStateBackend(new HashMapStateBackend())
    env.getCheckpointConfig.setCheckpointStorage("hdfs://master:9000/flink/checkpoint/")


    //读取kafka中的数据
    val source: KafkaSource[String] = KafkaSource
      .builder[String]
      .setBootstrapServers("master:9092") // kafka 集群的列表
      .setTopics("words") //消费的topic
      .setGroupId("my-group") //消费者组
      .setStartingOffsets(OffsetsInitializer.earliest()) //指定读取数据的位置
      .setValueOnlyDeserializer(new SimpleStringSchema()) //指定反序列化数据的类
      .build

    val linesDS: DataStream[String] = env.fromSource(source, WatermarkStrategy.noWatermarks(), "Kafka Source")


    val wordsDS: DataStream[String] = linesDS.flatMap(_.split(","))

    //按照单词分组
    val keyByDS: KeyedStream[String, String] = wordsDS.keyBy(word => word)

    //统计单词的数量
    val countDS: DataStream[(String, Long)] = keyByDS.process(new CountProcessFunction)

    countDS.print()

    env.execute()

  }
}

class CountProcessFunction extends KeyedProcessFunction[String, String, (String, Long)] {

  /**
    * count是一个普通的变量，数据保存Taskmanager的内存中，正常运行时可以一直累加
    * 如果任务执行失败重启了，count又会变成0
    *
    */
  //var count = 0


  var valueState: ValueState[Long] = _

  /**
    * open: processElement之前执行，一般用于初始化
    *
    */
  override def open(parameters: Configuration): Unit = {

    /**
      * valueState：单值状态，为每一个key保存一个值
      * 相当于内存中保存一个值，但是状态会被checkpoint持久化到hdfs中，如果任务失败可以恢复
      *
      */

    //1、定义一个状态
    //状态的描述对象
    val valueStateDesc = new ValueStateDescriptor[Long]("count", classOf[Long])
    valueState = getRuntimeContext.getState(valueStateDesc)

  }

  /**
    * 将数据一条一套传递给processElement函数
    *
    * @param word ：一个单词
    * @param ctx  ：上下文对象
    * @param out  ：用于将数据发送到下游
    */
  override def processElement(word: String,
                              ctx: KeyedProcessFunction[String, String, (String, Long)]#Context,
                              out: Collector[(String, Long)]): Unit = {

    //对count进行累加
    //valueStateDesc += 1

    //从状态中取数据，第一次里面的数据是类型的默认值
    //获取之前的统计结果
    val lastCount: Long = valueState.value()

    //再之前的基础上加一
    val count: Long = lastCount + 1

    //更新状态
    valueState.update(count)

    //将数据发送到下游
    out.collect((word, count))
  }
}
